package com.opennews.openplatform.service;

import com.opennews.openplatform.dto.user.UpdateUserDto;
import com.opennews.openplatform.dto.user.UpdateUserRolesDto;
import com.opennews.openplatform.jooq.DSLContextWrapper;
import com.opennews.openplatform.myspringbootcore.common.constant.RoleType;
import com.opennews.openplatform.myspringbootcore.util.SharedUtil;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.jooq.DSLContext;
import org.jooq.impl.DSL;
import org.springframework.core.env.Environment;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.stereotype.Service;

import java.io.File;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;

import static com.opennews.openplatform.entity.Tables.ACCOUNT_GROUP_USER;
import static com.opennews.openplatform.entity.Tables.USER;

@RequiredArgsConstructor
@Service
public class UserService {
    private final DSLContext dslContext;
    private final DSLContextWrapper dslContextWrapper;
    private final Environment env;
    private final UserActiveTokenService userActiveTokenService;
    private final UserRoleService userRoleService;

    /**
     * Masks username and avoids sensitive data revealing.
     */
    public String maskUsername(String username) {
        return String.format("%s****%s",
                username.substring(0, 3),
                username.substring(username.length() - 4));
    }

    /**
     * Validates if the presentedPassword matches the user stored password.
     *
     * @param username:          The string of the username.
     * @param presentedPassword: The string of the user provided password.
     * @return True means password matches. False means not.
     */
    public boolean validatePassword(String username, String presentedPassword) {
        String encodedPassword = dslContext.selectFrom(USER)
                .where(USER.USERNAME.eq(username))
                .fetchOne(USER.PASSWORD);

        if (!SharedUtil.isNullOrEmpty(encodedPassword)) {
            return PasswordEncoderFactories.createDelegatingPasswordEncoder().matches(presentedPassword, encodedPassword);
        }

        return false;
    }

    public Map<String, Object> queryStatus(String username) {
        return dslContext.select(
                        USER.ID.as("id"),
                        USER.USERNAME.as("username"),
                        USER.ENABLED.as("enabled"),
                        USER.ACCOUNT_EXPIRED.as("accountExpired"),
                        USER.ACCOUNT_LOCKED.as("accountLocked"),
                        USER.PASSWORD_EXPIRED.as("passwordExpired")
                )
                .from(USER)
                .where(USER.USERNAME.eq(username))
                .fetchOneMap();
    }

    /**
     * Checks if the username already exists.
     *
     * @param username: The username which will be checked of its existing.
     * @return True means the username exists. False means not.
     */
    public boolean checkOnUsername(String username) {
        return dslContextWrapper.count(USER, USER.USERNAME.eq(username)) > 0;
    }

    /**
     * Gets user avatar url based on its filename.
     *
     * @param avatarFilename: The avatar filename of the uploaded file.
     * @return The full url of user avatar.
     */
    public String getAvatarUrl(String avatarFilename) {
        if (avatarFilename == null) {
            avatarFilename = "";
        }

        String avatarFileFullName = Paths.get(env.getProperty("resource.upload.rootFilePath"), env.getProperty("resource.avatarDirectoryName"), avatarFilename).toString();

        if (SharedUtil.isNullOrEmpty(avatarFilename) || !(new File(avatarFileFullName).exists())) {
            return env.getProperty("resource.defaultAvatarUrl");
        }

        return String.format("%s/%s/%s",
                env.getProperty("resource.httpBaseUrl"),
                env.getProperty("resource.avatarDirectoryName"),
                avatarFilename);
    }

    /**
     * Queries the user details by id. This is for query account group user details.
     *
     * @param accountGroupId: The string of user account group id.
     * @param idOrUsername:   The string of id or username.
     * @return The map instance which contains keys below.
     * accountGroupId: String of user account group.
     * id: String of user id.
     * username: String of username.
     * fullName: String of user full name.
     * avatarUrl: String of user avatar url.
     * description: String of user description.
     * isAccountGroupCreator: Bool indicates if the user is account group registerer.
     * enabled: Bool indicates if the user is enabled or disabled.
     * fullRoles: Data returned by userRoleService.queryRoleWithIdAndTitles().
     * dateCreated: Date of the data created.
     * lastUpdated: Date of the data updated.
     */
    public Map<String, Object> queryDetails(String accountGroupId, String idOrUsername) {
        Map<String, Object> userDetails = dslContext.select(
                        ACCOUNT_GROUP_USER.ACCOUNT_GROUP_ID.as("accountGroupId"),
                        USER.ID.as("id"),
                        USER.USERNAME.as("username"),
                        USER.FULL_NAME.as("fullName"),
                        USER.AVATAR_FILENAME.as("avatarFilename"),
                        USER.DESCRIPTION.as("description"),
                        USER.ENABLED.as("enabled"),
                        USER.DATE_CREATED.as("dateCreated"),
                        USER.LAST_UPDATED.as("lastUpdated")
                )
                .from(ACCOUNT_GROUP_USER)
                .join(USER).on(ACCOUNT_GROUP_USER.USER_ID.eq(USER.ID))
                .where(
                        ACCOUNT_GROUP_USER.ACCOUNT_GROUP_ID.eq(accountGroupId).and(
                                DSL.or(
                                        USER.ID.eq(idOrUsername),
                                        USER.USERNAME.eq(idOrUsername)
                                )
                        )
                ).fetchOneMap();

        return fulfillDetails(accountGroupId, userDetails);
    }

    /**
     * Queries the user details by id. It is for authentication usage.
     *
     * @param idOrUsername: The string of id or username.
     * @return The map instance which contains keys below.
     * accountGroupId: String of user account group.
     * id: String of user id.
     * username: String of username.
     * fullName: String of user full name.
     * avatarUrl: String of user avatar url.
     * description: String of user description.
     * enabled: Bool indicates if the user is enabled or disabled.
     * fullRoles: Data returned by userRoleService.queryRoleWithIdAndTitles().
     * dateCreated: Date of the data created.
     * lastUpdated: Date of the data updated.
     */
    public Map<String, Object> queryDetails(String idOrUsername) {
        Map<String, Object> userDetails = dslContext.select(
                        USER.ACCOUNT_GROUP_ID.as("accountGroupId"),
                        USER.ID.as("id"),
                        USER.USERNAME.as("username"),
                        USER.FULL_NAME.as("fullName"),
                        USER.AVATAR_FILENAME.as("avatarFilename"),
                        USER.DESCRIPTION.as("description"),
                        USER.ENABLED.as("enabled"),
                        USER.DATE_CREATED.as("dateCreated"),
                        USER.LAST_UPDATED.as("lastUpdated")
                )
                .from(USER)
                .where(
                        DSL.or(
                                USER.ID.eq(idOrUsername),
                                USER.USERNAME.eq(idOrUsername)
                        )
                ).fetchOneMap();

        return fulfillDetails(null, userDetails);
    }

    /**
     * Queries the user ids by usernames.
     *
     * @param usernames: The string list of the usernames to query ids.
     * @return The string list of the user ids.
     */
    public List<String> queryUserIds(List<String> usernames) {
        if (usernames != null && !usernames.isEmpty()) {
            return dslContext.select(USER.ID)
                    .from(USER)
                    .where(USER.USERNAME.in(usernames))
                    .fetchInto(String.class);
        }

        return new ArrayList<>();
    }

    /**
     * Queries the user id by username.
     *
     * @param username: The string of the username to query its id.
     * @return The string of the user id.
     */
    public String queryUserId(String username) {
        List<String> result = queryUserIds(List.of(username));

        if (result != null) {
            return result.getFirst();
        }

        return null;
    }

    /**
     * Tries to query user basic info. The original value returned If user not found.
     *
     * @param accountGroupId: String of user account group id.
     * @param usernames:      String list of usernames.
     * @return The map instance which contains keys below.
     * id: String of user id.
     * username: String of user username.
     * fullName: String of user full name.
     * avatarUrl: String of user avatar url.
     */
    public List<Map<String, Object>> queryBasicInfo(String accountGroupId, List<String> usernames) {
        Set<String> uniqueUsernames = usernames != null ? new HashSet<>(usernames) : new HashSet<String>();
        List<Map<String, Object>> result = new ArrayList<>();

        if (!uniqueUsernames.isEmpty()) {
            List<Map<String, Object>> userDetails = dslContext.select(
                            USER.ID.as("id"),
                            USER.USERNAME.as("username"),
                            USER.FULL_NAME.as("fullName"),
                            USER.AVATAR_FILENAME.as("avatarFilename")
                    )
                    .from(USER)
                    .where(
                            USER.USERNAME.in(uniqueUsernames)
                                    .and(USER.ACCOUNT_GROUP_ID.eq(accountGroupId))
                    )
                    .fetchMaps();

            Map<String, Object> item;

            for (String username : uniqueUsernames) {
                item = userDetails.stream()
                        .filter(p -> p.get("username").equals(username))
                        .findFirst()
                        .orElse(null);

                if (item != null) {
                    result.add(Map.of(
                            "username", item.get("username"),
                            "fullName", item.get("fullName"),
                            "avatarUrl", getAvatarUrl((String) item.get("avatarFilename"))
                    ));
                } else {
                    result.add(Map.of(
                            "username", username,
                            "fullName", username,
                            "avatarUrl", getAvatarUrl("")
                    ));
                }
            }
        }

        return result;
    }

    /**
     * Queries users who are not in the provided account group.
     *
     * @param accountGroupId: String of user account group id.
     * @param keyword:        The string of the user account group id.
     * @param pageSize:       How many records is return in one trip.
     * @param pageIndex:      Where to take rows.
     * @return The map instance which contains keys below.
     * id: String of user id.
     * username: String of username.
     * fullName: String of user full name.
     * avatarUrl: String of user avatar url.
     */
    public List<Map<String, Object>> queryUsersNotInAccountGroup(String accountGroupId, String keyword, int pageSize, int pageIndex) {
        // We have to exclude these users:
        // - The owner of this account group.
        // - The user has this account group as his/her current account group.
        // - The user has this account group as NOT his/her current account group.

        // Queries all users which belongs to this account group.
        var excludedUserIds = dslContext.select(ACCOUNT_GROUP_USER.USER_ID)
                .from(ACCOUNT_GROUP_USER)
                .where(ACCOUNT_GROUP_USER.ACCOUNT_GROUP_ID.eq(accountGroupId));

        return dslContext.select(
                        USER.ID.as("id"),
                        USER.USERNAME.as("username"),
                        USER.FULL_NAME.as("fullName"),
                        USER.AVATAR_FILENAME.as("avatarFilename")
                )
                .from(ACCOUNT_GROUP_USER)
                .join(USER).on(ACCOUNT_GROUP_USER.USER_ID.eq(USER.ID))
                .where(
                        USER.USERNAME.like(keyword + "%")
                                .and(ACCOUNT_GROUP_USER.IS_ACCOUNT_GROUP_CREATOR.isTrue())
                                .and(ACCOUNT_GROUP_USER.USER_ID.notIn(excludedUserIds))
                )
                .limit(pageSize)
                .offset(pageIndex * pageSize)
                .fetch()
                .map(p -> {
                    Map<String, Object> item = p.intoMap();
                    item.put("username", maskUsername(item.get("username").toString()));
                    item.put("avatarUrl", getAvatarUrl((String) item.get("avatarFilename")));

                    return item;
                });
    }

    /**
     * Queries account group users by account group id.
     *
     * @param accountGroupId: The string of the user account group id.
     * @param pageSize:       How many records is return in one trip.
     * @param pageIndex:      Where to take rows.
     * @return The map instance which contains keys below.
     * id: String of user id.
     * username: String of username.
     * fullName: String of user full name.
     * avatarUrl: String of user avatar url.
     * enabled: Bool indicates if the user is enabled or disabled.
     * isAccountGroupCreator: Bool indicates if the user is account group registerer.
     * isAccountGroupAdmin: Bool indicates if the user is account group admin.
     */
    public List<Map<String, Object>> queryAccountGroupUsers(String accountGroupId, int pageSize, int pageIndex) {
        return dslContext.select(
                        USER.ID.as("id"),
                        USER.USERNAME.as("username"),
                        USER.FULL_NAME.as("fullName"),
                        USER.AVATAR_FILENAME.as("avatarFilename"),
                        USER.ENABLED.as("enabled"),
                        ACCOUNT_GROUP_USER.IS_ACCOUNT_GROUP_CREATOR.as("isAccountGroupCreator")
                ).from(ACCOUNT_GROUP_USER)
                .join(USER).on(ACCOUNT_GROUP_USER.USER_ID.eq(USER.ID))
                .where(ACCOUNT_GROUP_USER.ACCOUNT_GROUP_ID.eq(accountGroupId))
                .limit(pageSize)
                .offset(pageIndex * pageSize)
                .fetch()
                .map(p -> {
                    Map<String, Object> item = p.intoMap();
                    item.put("avatarUrl", getAvatarUrl((String) item.get("avatarFilename")));
                    item.put("isAccountGroupAdmin", userRoleService.hasRole(accountGroupId, p.get("id").toString(), RoleType.ROLE_ACCOUNT_GROUP_ADMIN));

                    return item;
                });
    }

    /**
     * Restores this user current account group to his/her own one.
     *
     * @param userId: String of user id.
     */
    public void restoreAccountGroupByUserId(String userId) {
        var defaultAccountGroupId = dslContext.select(ACCOUNT_GROUP_USER.ACCOUNT_GROUP_ID)
                .from(ACCOUNT_GROUP_USER)
                .where(
                        ACCOUNT_GROUP_USER.USER_ID.eq(userId)
                                .and(ACCOUNT_GROUP_USER.IS_ACCOUNT_GROUP_CREATOR.isTrue())
                );

        dslContext.update(USER)
                .set(USER.ACCOUNT_GROUP_ID, defaultAccountGroupId)
                .where(USER.ID.eq(userId))
                .execute();
    }

    /**
     * Account group with the input id is about to be deleted.
     * All users with this account group id as current one will be restored to their own account group id.
     *
     * @param accountGroupId: String of account group id which is about to be deleted.
     */
    public void restoreAccountGroupByAccountGroupId(String accountGroupId) {
        dslContext.update(USER)
                .set(USER.ACCOUNT_GROUP_ID,
                        // Subquery to select the account group ID
                        dslContext.select(ACCOUNT_GROUP_USER.ACCOUNT_GROUP_ID)
                                .from(ACCOUNT_GROUP_USER)
                                .where(ACCOUNT_GROUP_USER.USER_ID.eq(USER.ID)  // Reference to outer USER table
                                        .and(ACCOUNT_GROUP_USER.IS_ACCOUNT_GROUP_CREATOR.isTrue()))
                                .limit(1))  // Ensures only one account group ID is selected per user
                .where(USER.ACCOUNT_GROUP_ID.eq(accountGroupId))  // Update condition for users in the provided account group
                .execute();
    }

    /**
     * Updates user info based on provided map data and username.
     * The map keys must match user property names.
     *
     * @param username: The string of the username whose data will be updated.
     * @param data:     The map which contains keys with same name as user properties.
     */
    public void updateCurrentUser(String username, @Valid UpdateUserDto data) {
        dslContext.update(USER)
                .set(USER.FULL_NAME, data.getFullName())
                .set(USER.DESCRIPTION, data.getDescription())
                .where(USER.USERNAME.eq(username))
                .execute();
    }

    /**
     * Updates user related roles.
     *
     * @param accountGroupId: The string of current user account group id.
     * @param data:           The map which contains keys with same name as user properties.
     */
    public void updateRoles(String accountGroupId, @Valid UpdateUserRolesDto data) {
        String userId = data.getId();
        String username = data.getUsername();

        // Makes sure current user and parameter user belong to same user account group.
        // Makes sure account group owner's role cannot be changed.
        if (dslContextWrapper.count(ACCOUNT_GROUP_USER,
                ACCOUNT_GROUP_USER.ACCOUNT_GROUP_ID.eq(accountGroupId)
                        .and(ACCOUNT_GROUP_USER.USER_ID.eq(userId)
                        )) == 1 &&
                dslContextWrapper.count(ACCOUNT_GROUP_USER,
                        ACCOUNT_GROUP_USER.ACCOUNT_GROUP_ID.eq(accountGroupId)
                                .and(ACCOUNT_GROUP_USER.USER_ID.eq(userId)
                                        .and(ACCOUNT_GROUP_USER.IS_ACCOUNT_GROUP_CREATOR.isTrue()
                                        ))) == 0) {
            List<Map<String, Object>> fullRoles = data.getFullRoles();

            // If the user roles is set.
            if (fullRoles != null && !fullRoles.isEmpty()) {
                // Gets the role id list from the posted data.
                List<String> roleIds = SharedUtil.<String>mapListMap(fullRoles.stream(), "id").collect(Collectors.toList());

                // Queries user current role ids.
                List<String> currentRoleIds = userRoleService.queryRoleIds(accountGroupId, userId);

                // Gets all new added role ids.
                // Gets all new added role ids (roleIds - currentRoleIds).
                Set<String> addedRoleIds = new HashSet<>(roleIds);
                currentRoleIds.forEach(addedRoleIds::remove);

                // Gets all deleted role ids.
                Set<String> deletedRoleIds = new HashSet<>(currentRoleIds);
                roleIds.forEach(deletedRoleIds::remove);

                // Adds new roles if needed.
                if (!addedRoleIds.isEmpty()) {
                    userRoleService.add(accountGroupId, userId, new ArrayList<>(addedRoleIds));
                }

                // Deletes roles if needed.
                if (!deletedRoleIds.isEmpty()) {
                    userRoleService.delete(accountGroupId, userId, new ArrayList<>(deletedRoleIds));
                }

                // If role has been changed we need to make user re-login by deleting active tokens.
                if (!addedRoleIds.isEmpty() || !deletedRoleIds.isEmpty()) {
                    userActiveTokenService.deleteByUserAndAccountGroup(username, accountGroupId);
                }
            }
        }
    }

    /**
     * Updates the password for the user with provided username.
     *
     * @param currentUserAccountGroupId: The string of current user account group id.
     * @param inputUsername:             The string of the input username.
     * @param password:                  The string of the password.
     */
    public void updatePassword(String currentUserAccountGroupId, String inputUsername, String password) {
        // beforeUpdate method in User only triggered by save method.
        // That's why we need to encode password manually.
        dslContext.update(USER)
                .set(USER.PASSWORD, PasswordEncoderFactories.createDelegatingPasswordEncoder().encode(password))
                .where(
                        USER.USERNAME.eq(inputUsername)
                                .and(USER.ACCOUNT_GROUP_ID.eq(currentUserAccountGroupId))
                )
                .execute();
    }

    /**
     * Fulfills queried user data with extra query.
     *
     * @param accountGroupId: The string of the user account group id.
     * @param userDetails:    The details data queried from db.
     * @return The userDetails with fulfilled extra details.
     */
    private Map<String, Object> fulfillDetails(String accountGroupId, Map<String, Object> userDetails) {
        if (userDetails != null) {
            // Sets the avatar url based on its filename.
            userDetails.put("avatarUrl", getAvatarUrl((String) userDetails.get("avatarFilename")));

            // Checks if current account group creator.
            if (accountGroupId == null) {
                accountGroupId = userDetails.get("accountGroupId").toString();
            }

            // Checks if current account group creator.
            userDetails.put("isAccountGroupCreator", dslContextWrapper.count(ACCOUNT_GROUP_USER,
                    ACCOUNT_GROUP_USER.ACCOUNT_GROUP_ID.eq(accountGroupId)
                            .and(ACCOUNT_GROUP_USER.USER_ID.eq(userDetails.get("id").toString())
                                    .and(ACCOUNT_GROUP_USER.IS_ACCOUNT_GROUP_CREATOR.isTrue()
                                    ))) > 0);

            // Queries user role titles.
            userDetails.put("fullRoles", userRoleService.queryFullRoles(userDetails.get("accountGroupId").toString(), userDetails.get("id").toString()));
        }

        return userDetails;
    }
}